package com.hnit.applet.common.aspect;

import com.hnit.applet.common.annotation.AccessLimit;
import com.hnit.applet.common.enums.LimitType;
import com.hnit.applet.common.enums.ResponseStatus;
import com.hnit.applet.common.exception.AppRuntimeException;
import com.hnit.applet.common.utils.IpUtil;
import com.hnit.applet.common.utils.RedisUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * @author zwj
 * @date 2022/7/13
 * @description TODO 注解AccessLimit切面
 */
@Component
@Aspect
public class AccessLimitAspect {

    @Resource
    private RedisUtil redisUtil;

    /**
     * 配置切入点
     */
    @Pointcut("@annotation(com.hnit.applet.common.annotation.AccessLimit)")
    public void accessLimitPointcut() {
        // 该方法无方法体,主要为了让同类中其他方法使用此切入点
    }


    /**
     * 前置通知：目标方法执行之前执行以下方法体的内容
     *
     * @param joinPoint：提供对连接点处可用状态和有关它的静态信息的反射访问
     */
    @Before("accessLimitPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        AccessLimit annotation = methodSignature.getMethod().getAnnotation(AccessLimit.class);
        // 拼接key
        String key = getCombineKey(annotation, joinPoint);
        if (redisUtil.hasKey(key)) {
            redisUtil.incr(key, 1);
            if (Integer.parseInt(redisUtil.get(key).toString()) > annotation.maxCount()) {
                throw new AppRuntimeException(ResponseStatus.TOO_MANY_REQUESTS.value(), String.valueOf(ResponseStatus.TOO_MANY_REQUESTS.value()),
                        ResponseStatus.TOO_MANY_REQUESTS.getReasonPhrase());
            }
        } else {
            redisUtil.set(key, 1, annotation.seconds());
        }
    }

    /**
     * 后置通知：目标方法执行之后执行以下方法体的内容，不管目标方法是否发生异常。
     */
    @After(value = "accessLimitPointcut()")
    public void aspectAfter(JoinPoint joinPoint) {
    }

    /**
     * 返回通知：目标方法返回后执行以下代码
     * value 属性：绑定通知的切入点表达式。可以关联切入点声明，也可以直接设置切入点表达式
     * pointcut 属性：绑定通知的切入点表达式，优先级高于 value，默认为 ""
     * returning 属性：通知签名中要将返回值绑定到的参数的名称，默认为 ""
     *
     * @param joinPoint ：提供对连接点处可用状态和有关它的静态信息的反射访问
     * @param result    ：目标方法返回的值，参数名称与 returning 属性值一致。无返回值时，这里 result 会为 null.
     */
    @AfterReturning(pointcut = "accessLimitPointcut()", returning = "result")
    public void aspectAfterReturning(JoinPoint joinPoint, Object result) {
    }


    /**
     * 异常通知：目标方法发生异常的时候执行以下代码，此时返回通知不会再触发
     * value 属性：绑定通知的切入点表达式。可以关联切入点声明，也可以直接设置切入点表达式
     * pointcut 属性：绑定通知的切入点表达式，优先级高于 value，默认为 ""
     * throwing 属性：与方法中的异常参数名称一致，
     *
     * @param joinPoint join point for advice
     * @param e         exception
     */
    @AfterThrowing(pointcut = "accessLimitPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {

    }

    /**
     * 配置环绕通知
     *
     * @param joinPoint join point for advice
     */
    @Around("accessLimitPointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result;
        result = joinPoint.proceed();
        return result;
    }

    /**
     * 根据限流类型拼接key
     */
    public String getCombineKey(AccessLimit limit, JoinPoint point) {
        StringBuilder sb = new StringBuilder(limit.prefix());
        // 按照IP限流
        if (limit.type() == LimitType.IP) {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            assert requestAttributes != null;
            HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
            assert request != null;
            sb.append(IpUtil.getIpAddr(request)).append("-");
        }
        // 拼接类名和方法名
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        sb.append(targetClass.getName()).append("-").append(method.getName());
        return sb.toString();
    }

}
